package com.basker.pisces.orm.repository;

import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import com.basker.pisces.core.PageRowList;
import com.basker.pisces.core.RowContract;
import com.basker.pisces.core.exception.DataRepositoryException;
import com.basker.pisces.core.helper.DataContractHelper;
import com.basker.pisces.core.helper.DataContractHelper.DataContractCopyOption;
import com.basker.pisces.core.meta.IDataField;
import com.basker.pisces.core.meta.IDataObjectMeta;
import com.basker.pisces.core.meta.bean.BeanMetaManager;
import com.basker.pisces.core.meta.utils.DataObjectMetaUtils;
import com.basker.pisces.core.query.IQuery;
import com.basker.pisces.core.query.IQuery.PagingInfo;
import com.basker.pisces.core.query.QueryInfo;
import com.basker.pisces.core.query.builder.CriterionBuilder;
import com.basker.pisces.core.query.builder.CriterionBuilder.Operator;
import com.basker.pisces.core.query.builder.QueryBuilder;
import com.basker.pisces.core.query.meta.QueryMeta;
import com.basker.pisces.core.query.meta.bean.BeanQueryMeta;
import com.basker.pisces.core.repository.IBeanRepository;
import com.basker.pisces.core.repository.IDataRepository;
import com.basker.pisces.core.utils.ConvertUtils;
import com.basker.pisces.exception.ExceptionFactory;
import com.basker.pisces.orm.query.DefaultQuery;
import com.basker.pisces.orm.session.ISqlSession;
import com.basker.pisces.orm.session.SqlSessionFactoryManager;
import com.basker.pisces.resource.StringResources;

public class DefaultRepository implements IBeanRepository, IDataRepository {

    private static final Logger logger = LoggerFactory.getLogger(DefaultRepository.class);

    /**
     * 拷贝用于更新的数据对象的参数，除了业务字段，还拷贝主键和数据状态字段，但是引用字段不拷贝
     */
    private final static DataContractCopyOption COPY_OPTION;

    static {
        COPY_OPTION = new DataContractCopyOption();
        COPY_OPTION.setCopyPkValue(true);
        COPY_OPTION.setCopyDataContractStatus(true);
    }

    @Override
    public void delete(IDataObjectMeta objectMeta, Object id) {
        this.checkisArray(id, "id");
        this.deleteBatch(objectMeta, new Object[]{id});
    }

    @Override
    public void deleteBatch(IDataObjectMeta objectMeta, Object[] ids) {
        this.workInSqlSession(objectMeta, "delete", (sqlSession) -> {
            sqlSession.delete(objectMeta, ids);
            return null;
        });
    }

    @Override
    public <TBean> void deleteBean(Class<TBean> clazz, Object id) {
        this.checkisArray(id, "id");
        this.deleteBeans(clazz, new Object[]{id});
    }

    @Override
    public <TBean> void deleteBeans(Class<TBean> clazz, Object[] ids) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        this.deleteBatch(objectMeta, ids);
    }

    @Override
    public <TBean> void fillRefrenceFields(Class<TBean> clazz, TBean[] dataObjects) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        this.fillReferenceFields(objectMeta, dataObjects);
    }

    @Override
    public void fillReferenceFields(IDataObjectMeta objectMeta, Object[] dataObjects) {
        this.workInSqlSession(objectMeta, "fillRefrenceFields", sqlSession -> {
            sqlSession.fillReferenceFields(objectMeta, dataObjects);
            return null;
        });
    }

    @Override
    public Object load(IDataObjectMeta objectMeta, Object id) {
        this.checkisArray(id, "id");

        Object[] objects = this.internalLoad(objectMeta, new Object[]{id}, false);

        if (objects.length > 0) {
            return objects[0];
        }

        return null;
    }

    @Override
    public Object[] loadBatch(IDataObjectMeta objectMeta, Object[] ids) {
        return this.internalLoad(objectMeta, ids, false);
    }

    @Override
    public Object[] loadBatchForUpdate(IDataObjectMeta objectMeta, Object[] ids) {
        return this.internalLoad(objectMeta, ids, true);
    }

    @Override
    public <TBean> TBean loadBean(Class<TBean> clazz, Object id) {
        this.checkisArray(id, "id");

        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, new Object[]{id}, false);

        if (beans.length > 0) {
            return beans[0];
        }

        return null;
    }

    @Override
    public <TBean> TBean loadBeanForUpdate(Class<TBean> clazz, Object id) {
        this.checkisArray(id, "id");

        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, new Object[]{id}, true);

        if (beans.length > 0) {
            return beans[0];
        }

        return null;
    }

    @Override
    public <TBean> TBean[] loadBeans(Class<TBean> clazz, Object[] ids) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, ids, false);
        return beans;
    }

    @Override
    public <TBean> TBean[] loadBeansForUpdate(Class<TBean> clazz, Object[] ids) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, ids, true);
        return beans;
    }

    @Override
    public Object loadForUpdate(IDataObjectMeta objectMeta, Object id) {
        this.checkisArray(id, "id");

        Object[] objects = this.internalLoad(objectMeta, new Object[]{id}, true);

        if (objects.length > 0) {
            return objects[0];
        }

        return null;
    }

    @Override
    public <TBean> TBean loadPartBean(Class<TBean> clazz, Collection<String> loadFields, Object id) {
        this.checkisArray(id, "id");

        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, new Object[]{id}, false);

        if (beans.length > 0) {
            return beans[0];
        }

        return null;
    }

    @Override
    public <TBean> TBean loadPartBeanForUpdate(Class<TBean> clazz, Collection<String> loadFields, Object id) {
        this.checkisArray(id, "id");

        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, new Object[]{id}, true);

        if (beans.length > 0) {
            return beans[0];
        }

        return null;
    }

    @Override
    public <TBean> TBean[] loadPartBeans(Class<TBean> clazz, Collection<String> loadFields, Object[] ids) {
        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, ids, false);
        return beans;
    }

    @Override
    public <TBean> TBean[] loadPartBeansForUpdate(Class<TBean> clazz, Collection<String> loadFields, Object[] ids) {
        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalLoad(objectMeta, ids, true);
        return beans;
    }

    @Override
    public Object[] query(IDataObjectMeta objectMeta, String whereSql, Object... args) {
        return this.internalQuery(objectMeta, false, whereSql, args);
    }

    @Override
    public <TBean> TBean queryBean(Class<TBean> clazz, CriterionBuilder... builders) {
        return this.fetchOne(this.queryBeans(clazz, builders));
    }

    @Override
    public <TBean> TBean queryBean(Class<TBean> clazz, String whereSql, Object... args) {
        return this.fetchOne(this.queryBeans(clazz, whereSql, args));
    }

    @Override
    public <TBean> TBean queryBeanForUpdate(Class<TBean> clazz, CriterionBuilder... builders) {
        return this.fetchOne(this.queryBeansForUpdate(clazz, builders));
    }

    @Override
    public <TBean> TBean queryBeanForUpdate(Class<TBean> clazz, String whereSql, Object... args) {
        return this.fetchOne(this.queryBeansForUpdate(clazz, whereSql, args));
    }

    @Override
    public <TBean> TBean[] queryBeans(Class<TBean> clazz, CriterionBuilder... builders) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        QueryMeta queryMeta = this.getQueryMeta(clazz);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, queryMeta, false, builders);
        return beans;
    }

    @Override
    public <TBean> TBean[] queryBeans(Class<TBean> clazz, String whereSql, Object... args) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, false, whereSql, args);
        return beans;
    }

    @Override
    public <TBean> TBean[] queryBeansForUpdate(Class<TBean> clazz, CriterionBuilder... builders) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        QueryMeta queryMeta = this.getQueryMeta(clazz);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, queryMeta, true, builders);
        return beans;
    }

    @Override
    public <TBean> TBean[] queryBeansForUpdate(Class<TBean> clazz, String whereSql, Object... args) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, true, whereSql, args);
        return beans;
    }

    @Override
    public <T> T queryForObject(QueryMeta queryMeta, QueryInfo queryInfo, Class<T> returnType) {
        List<RowContract> rows = this.queryForRows(queryMeta, queryInfo);

        if (CollectionUtils.isEmpty(rows)) {
            return null;
        }

        Iterator<Object> iterator = rows.get(0).values().iterator();
        if (iterator.hasNext()) {
            Object val = iterator.next();
            return val != null ? ConvertUtils.convert(val, returnType) : null;
        } else {
            return null;
        }
    }

    @Override
    public PageRowList queryForPageRows(QueryMeta queryMeta, QueryInfo queryInfo, PagingInfo pagingInfo) {
        IQuery query = this.getQuery(queryMeta);
        return query.queryForPageRows(queryInfo, pagingInfo);
    }

    @Override
    public int queryForRowCount(QueryMeta queryMeta, QueryInfo queryInfo) {
        IQuery query = this.getQuery(queryMeta);
        return query.queryForRowCount(queryInfo);
    }

    @Override
    public List<RowContract> queryForRows(QueryMeta queryMeta, QueryInfo queryInfo) {
        IQuery query = this.getQuery(queryMeta);
        return query.queryForRows(queryInfo);
    }

    @Override
    public Object[] queryForUpdate(IDataObjectMeta objectMeta, String whereSql, Object... args) {
        return this.internalQuery(objectMeta, true, whereSql, args);
    }

    @Override
    public <TBean> TBean queryPartBean(Class<TBean> clazz, Collection<String> loadFields,
                                       CriterionBuilder... builders) {
        return this.fetchOne(this.queryPartBeans(clazz, loadFields, builders));
    }

    @Override
    public <TBean> TBean queryPartBean(Class<TBean> clazz, Collection<String> loadFields, String whereSql,
                                       Object... args) {
        return this.fetchOne(this.queryPartBeans(clazz, loadFields, whereSql, args));
    }

    @Override
    public <TBean> TBean queryPartBeanForUpdate(Class<TBean> clazz, Collection<String> loadFields,
                                                CriterionBuilder... builders) {
        return this.fetchOne(this.queryPartBeansForUpdate(clazz, loadFields, builders));
    }

    @Override
    public <TBean> TBean queryPartBeanForUpdate(Class<TBean> clazz, Collection<String> loadFields, String whereSql,
                                                Object... args) {
        return this.fetchOne(this.queryPartBeansForUpdate(clazz, loadFields, whereSql, args));
    }

    @Override
    public <TBean> TBean[] queryPartBeans(Class<TBean> clazz, Collection<String> loadFields,
                                          CriterionBuilder... builders) {
        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }

        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);
        QueryMeta queryMeta = this.getQueryMeta(clazz);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, queryMeta, false, builders);
        return beans;
    }

    @Override
    public <TBean> TBean[] queryPartBeans(Class<TBean> clazz, Collection<String> loadFields, String whereSql,
                                          Object... args) {
        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }

        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, false, whereSql, args);
        return beans;
    }

    @Override
    public <TBean> TBean[] queryPartBeansForUpdate(Class<TBean> clazz, Collection<String> loadFields,
                                                   CriterionBuilder... builders) {
        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }

        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);
        QueryMeta queryMeta = this.getQueryMeta(clazz);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, queryMeta, true, builders);
        return beans;
    }

    @Override
    public <TBean> TBean[] queryPartBeansForUpdate(Class<TBean> clazz, Collection<String> loadFields, String whereSql,
                                                   Object... args) {
        if (loadFields == null) {
            loadFields = Collections.emptySet();
        }
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz, loadFields);

        @SuppressWarnings("unchecked")
        TBean[] beans = (TBean[]) this.internalQuery(objectMeta, true, whereSql, args);
        return beans;
    }

    @Override
    public Object save(IDataObjectMeta objectMeta, Object dataObject) {
        this.checkisArray(dataObject, "dataObject");
        Object[] ids = this.saveBatch(objectMeta, new Object[]{dataObject});
        return ids.length > 0 ? ids[0] : null;

    }

    @Override
    public Object[] saveBatch(IDataObjectMeta objectMeta, Object[] dataObjects) {
        return this.workInSqlSession(objectMeta, "save", (sqlSession) -> {
            return sqlSession.save(objectMeta, dataObjects);
        });
    }

    @Override
    public <TBean> void saveBean(Class<TBean> clazz, Object bean) {
        this.checkisArray(bean, "bean");
        this.saveBeans(clazz, new Object[]{bean});
    }

    @Override
    public <TBean> void saveBeans(Class<TBean> clazz, Object[] beans) {
        IDataObjectMeta objectMeta = this.getBeanMeta(clazz);
        this.saveBatch(objectMeta, beans);
    }

    private void checkisArray(Object arg, String argName) {
        if (arg == null) {
            throw new IllegalArgumentException("parameter '" + argName + "' is required.");
        }

        if (arg.getClass().isArray()) {
            throw ExceptionFactory.createRuntimeException("pisces-orm.DefaultRepository.checkisArray-001",
                    "argument:{0} is an array!", argName);
        }
    }

    private boolean checkReadOnly(IDataObjectMeta objectMeta) {
        if (objectMeta.isReadOnly()) {
            if (logger.isWarnEnabled()) {
                logger.warn(
                        MessageFormat.format("pisces-orm.DefaultRepository.checkReadOnly-001", objectMeta.getName()));
            }

            return false;
        }

        return true;
    }

    private ISqlSession createSqlSession(IDataObjectMeta objectMeta) {
        String schema = objectMeta.getSchema();
        return SqlSessionFactoryManager.getDfaultFactory().createSession(schema, Isolation.READ_COMMITTED);
    }

    @SuppressWarnings("unchecked")
    private <TBean> TBean fetchOne(Object[] objects) {
        Object object = ArrayUtils.isNotEmpty(objects) ? objects[0] : null;
        return (TBean) object;
    }

    private IDataObjectMeta getBeanMeta(Class<?> clazz) {
        return BeanMetaManager.getBeanMeta(clazz);
    }

    private IDataObjectMeta getBeanMeta(Class<?> clazz, Collection<String> loadFields) {
        IDataObjectMeta objectMeta = BeanMetaManager.getBeanMeta(clazz);

        if (loadFields == null) {
            // null,表示完整的结构
        } else {
            if (loadFields.isEmpty()) {
                // empty，则默认加载id代码名称字段
                List<String> idCodeNameFields = DataObjectMetaUtils.getIdCodeNameFields(objectMeta);
                loadFields = new LinkedHashSet<>(idCodeNameFields);
            } else {
                loadFields = new LinkedHashSet<>(loadFields);
            }

            objectMeta = BeanMetaManager.getPartBeanMeta(clazz, loadFields.toArray(new String[loadFields.size()]));
        }

        return objectMeta;
    }

    private IQuery getQuery(QueryMeta queryMeta) {
        return new DefaultQuery(queryMeta);
    }

    private QueryMeta getQueryMeta(Class<?> clazz) {
        return BeanQueryMeta.get(clazz);
    }

    private void handleForUpdate(IDataObjectMeta objectMeta, boolean forUpdate, Object[] objects) {
        // 如果是forUpdate,那么需要检查元信息是否可以修改
        if (forUpdate && this.checkReadOnly(objectMeta)) {

            // 如果可以修改，那么需要将数据克隆，否则会引起并发修改的问题
            for (int i = 0; i < objects.length; i++) {
                objects[i] = DataContractHelper.copy(objects[i], COPY_OPTION);
            }
        }

        // 最后对数据的readOnly状态进行设置
        for (int i = 0; i < objects.length; i++) {
            DataContractHelper.markReadOnly(objects[i], !forUpdate);
        }
    }

    private Object[] internalLoad(IDataObjectMeta objectMeta, Object[] ids, boolean forUpdate) {
        Object[] objects = this.workInSqlSession(objectMeta, "load", (sqlSession) -> {
            Object[] dataObjects = sqlSession.loadBatch(objectMeta, ids, true, forUpdate);
            sqlSession.fillReferenceFields(objectMeta, dataObjects);
            return dataObjects;
        });

        if (!ArrayUtils.isEmpty(objects)) {
            this.handleForUpdate(objectMeta, forUpdate, objects);
        }

        return objects;
    }

    private Object[] internalQuery(IDataObjectMeta objectMeta, boolean forUpdate, String whereSql, Object... args) {
        Object[] objects = this.workInSqlSession(objectMeta, "query", (sqlSession) -> {
            Object[] dataObjects = sqlSession.queryForObjects(objectMeta, whereSql, args, new int[0], true, forUpdate);
            sqlSession.fillReferenceFields(objectMeta, dataObjects);
            return dataObjects;
        });

        if (!ArrayUtils.isEmpty(objects)) {
            this.handleForUpdate(objectMeta, forUpdate, objects);
        }

        return objects;
    }

    private Object[] internalQuery(IDataObjectMeta objectMeta, QueryMeta queryMeta, boolean forUpdate,
                                   CriterionBuilder... builders) {
        IDataField pkField = objectMeta.getPkField();

        Assert.notNull(pkField, "objectMeta's pkField is required");

        if (builders.length == 1) {
            // 如果是针对主键的条件，直接调用主键加载的方法
            Object[] objects = this.tryLoadById(objectMeta, forUpdate, builders[0]);
            if (objects != null) {
                return objects;
            }
        }

        QueryInfo queryInfo = QueryBuilder.create(queryMeta).select(pkField.getName()).andCriteria(builders)
                .buildQueryInfo();
        IQuery query = this.getQuery(queryMeta);

        List<RowContract> rows = query.queryForRows(queryInfo);

        Object[] ids = rows.stream().map(r -> r.get(pkField.getName())).toArray();
        Object[] objects = this.internalLoad(objectMeta, ids, forUpdate);

        return objects;
    }

    /**
     * 如果criterionBuilder是关联主键的等值或IN条件，直接调用{@link #internalLoad(IDataObjectMeta, Object[], boolean)}
     *
     * @param objectMeta
     * @param forUpdate
     * @param criterionBuilder
     * @return
     */
    private Object[] tryLoadById(IDataObjectMeta objectMeta, boolean forUpdate, CriterionBuilder criterionBuilder) {
        IDataField pkField = objectMeta.getPkField();
        if (!Objects.equals(pkField.getName(), criterionBuilder.getField())) {
            return null;
        }

        Operator operator = criterionBuilder.getOperator();
        Object value = criterionBuilder.getValue();

        Object[] ids = null;

        if (Operator.Equal.equals(operator)) {
            ids = ArrayUtils.toArray(value);
            return this.internalLoad(objectMeta, ArrayUtils.toArray(value), forUpdate);
        }

        if (Operator.In.equals(operator)) {
            ids = ConvertUtils.convert2Array(value);
        }

        if (ids != null) {
            return this.internalLoad(objectMeta, ids, forUpdate);
        }

        return null;
    }

    private <R> R workInSqlSession(IDataObjectMeta objectMeta, String methodName, Function<ISqlSession, R> funtion) {
        try (ISqlSession sqlSession = this.createSqlSession(objectMeta)) {
            return funtion.apply(sqlSession);
        } catch (Exception e) {
            String message = StringResources
                    .get("pisces-orm." + this.getClass().getSimpleName() + "." + methodName + "-001", e.getMessage());
            throw new DataRepositoryException(message, e);
        }
    }

}
